package shake;

import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import shake.context.ComponentRegistry;
import shake.context.NameResolver;
import shake.deployment.ComponentScanner;
import shake.event.EventAgent;
import shake.init.module.ComponentDescriptor;
import shake.init.module.EventModule;
import shake.init.module.FacesContextModule;
import shake.init.module.Log4jModule;
import shake.init.module.ShakeScopeModule;

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Singleton;
import com.google.inject.Stage;
import com.google.inject.servlet.ServletModule;

public class Shake {
	static Logger logger = LogManager.getLogger(Shake.class);

	static public Injector create(Stage stage, List<Module> module,
			ClassLoader loader) {
		module.add(new ServletModule());
		module.add(new ShakeScopeModule());
		module.add(new Log4jModule());
		module.add(new FacesContextModule());

		HashMap<Key<?>, ComponentDescriptor> installComponents = scanComponents(loader);
		addComponentModule(module, installComponents);
		addEventModule(module, installComponents);

		Injector injector = Guice.createInjector(stage, module);

		installObserverAndNameResolver(module, injector);
		startupSingletonComponent(module, injector);
		return injector;
	}

	private static void addComponentModule(List<Module> module,
			HashMap<Key<?>, ComponentDescriptor> installComponents) {
		for (ComponentDescriptor com : installComponents.values()) {
			module.add(com);
			if (logger.isInfoEnabled()) {
				logger.info("install " + com.toString());
			}
		}
	}

	private static HashMap<Key<?>, ComponentDescriptor> scanComponents(
			ClassLoader loader) {
		try {
			ComponentScanner scanner = new ComponentScanner();
			scanner.scanResources(new String[] { "META-INF/shake.properties" },
					loader);
			Set<String> list = scanner.getComponentsClasses();
			HashMap<Key<?>, ComponentDescriptor> installComponents = new HashMap<Key<?>, ComponentDescriptor>();
			Properties properties = new Properties();
			properties.load(loader
					.getResourceAsStream("META-INF/install.properties"));
			for (String s : list) {

				Class<?> clazz = loader.loadClass(s);
				ComponentDescriptor com = new ComponentDescriptor(clazz,
						properties);
				if (!com.isInstall()) {
					continue;
				}
				com.google.inject.Key<?> type = com.getKey();
				if (installComponents.containsKey(type)) {
					int pre = installComponents.get(type).getPrecedence();
					int pre2 = com.getPrecedence();
					if (pre == pre2) {
						throw new RuntimeException(
								"same precedence component: "
										+ com.getComponentClass().getName()
										+ " and "
										+ installComponents.get(type)
												.getComponentClass().getName());
					}

					if (pre < pre2) {
						installComponents.put(type, com);
					}

					if (logger.isInfoEnabled()) {
						logger.info("same type component:"
								+ type
								+ " bind to "
								+ installComponents.get(type)
										.getComponentClass().getName());
					}
				} else {
					installComponents.put(type, com);
				}

			}
			return installComponents;
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	private static void addEventModule(List<Module> module,
			HashMap<Key<?>, ComponentDescriptor> installComponents) {
		Set<ParameterizedType> types = new HashSet<ParameterizedType>();
		for (ComponentDescriptor com : installComponents.values()) {
			for (ParameterizedType type : com.getInjectTypes()) {
				types.add(type);
			}
		}
		for (ParameterizedType type : types) {
			if (type.getRawType().equals(Event.class)) {
				module.add(new EventModule(type));
			}
		}
	}

	private static void installObserverAndNameResolver(List<Module> module,
			Injector injector) {
		ComponentRegistry registry = injector
				.getInstance(ComponentRegistry.class);
		NameResolver resolver = injector.getInstance(NameResolver.class);
		for (Module m : module) {
			if (m instanceof ComponentDescriptor) {
				((ComponentDescriptor) m).installObservers(injector);
				((ComponentDescriptor) m).installNamedResolver(resolver);
				registry.add((ComponentDescriptor) m);
			}
		}
	}

	private static void startupSingletonComponent(List<Module> module,
			Injector injector) {
		EventAgent e = injector.getInstance(EventAgent.class);
		for (Module m : module) {
			if (m instanceof ComponentDescriptor) {
				ComponentDescriptor com = (ComponentDescriptor) m;
				// System.out.println(com + "\t" + com.isStartup());
				if (com.isStartup()) {
					Class scopeAnnotation = com.getScopeAnnotation();
					if (Singleton.class.equals(scopeAnnotation)) {
						if (logger.isInfoEnabled()) {
							logger.info("startup " + com.toString());
						}
						Object o = injector.getInstance(com.getKey());
						e.getStartup().fireEvent(o);
					}
				}
			}
		}

	}

	// public static int toPrecedence(Class<?> componentClass) {
	// // if (componentClass.isAnnotationPresent(Install.class)) {
	// // return componentClass.getAnnotation(Install.class).precedence();
	// // }
	// // return Install.APPLICATION;
	// }

	static public Injector create(Stage stage, ClassLoader loader,
			Module... modules) {
		ArrayList<Module> m = new ArrayList<Module>();
		m.addAll(Arrays.asList(modules));
		return create(stage, m, loader);
	}

	static public Injector create(Stage stage, Module... modules) {
		return create(stage, Thread.currentThread().getContextClassLoader(),
				modules);
	}

}
